Repository navigation
fix(policy): keep an explicitly saved default as an enforceable system rule - #9039
maia-andre wants to merge 4 commits into
Conversation
…m rule An administrator could not enforce the built-in default of a policy. Saving the default with allowChildOverride = false deleted the system rule, and reading an explicit default always returned an overridable layer, so a group or user rule still changed the effective value. - PolicySource::saveSystemPolicy() stores every explicit value with its override flag, including a value equal to the default. - PolicySource::loadSystemPolicy() applies the stored flag to an explicit default. A default stored without a flag predates explicit defaults, when the API could not enforce one, so it stays overridable. - PolicyService::saveSystem() treats null as "no explicit system rule" and clears it, for single and compound writes. Policy Workbench already removes a rule this way, so it needs no change. The API documentation of both system write endpoints now states the difference between null and an explicit default. Resolves: LibreSign#8916 Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
Part of LibreSign#8916 Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
Saving a value equal to the default now keeps it as an explicit system rule, so the admin initial state scenarios and the validation access end-to-end test can no longer use the default value to clear the rule. They send null, which is how the workbench removes a system rule. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: André Maia <andrefnkmm@gmail.com>
YvesCesar
left a comment
There was a problem hiding this comment.
Hi, @maia-andre ! :)
Can you check this review?
An empty stored value was always read as "no explicit rule", so saving legal_information (default '') as enforced left a group or user layer in control and orphaned its override flag. The policy API writes the flag with every value, so only an empty value without it is treated as absent. Signed-off-by: André Maia <andrefnkmm@gmail.com>
YvesCesar
left a comment
There was a problem hiding this comment.
Thanks, this resolves what I raised: the read side is fixed and the new matrix covers it. I ran the Policy unit tests on 481967c: 972 pass.
With the read fixed, one related spot now gets out of sync (I missed it in my first comment): the rule count still ignores empty values.
hydrateExplicitSystemRuleCounts() filters configvalue != '', so an enforced legal_information = '' is active but gets everyoneCount = 0. The Policy Workbench catalog then shows it as if there were no system rule.
Suggestion: apply the same rule as loadSystemPolicy() and count an empty value when its .allow_child_override key exists, with a test for it.
Resolves: #8916
📝 Summary
An administrator could not enforce the built-in default of a policy. Saving it with
allowChildOverride = falsewas lost on both sides:PolicySource::saveSystemPolicy()deleted the system rule when the value equaled the default and the override was disabled.PolicySource::loadSystemPolicy()returned an overridable layer for an explicit default, whatever its flag. A group or user rule still changed the effective value.What changes:
saveSystemPolicy()stores every explicit value with its override flag, including a value equal to the default. The special case that deleted the rule, including its JSON comparison, is gone.loadSystemPolicy()applies the stored flag to an explicit default. A default stored without a flag predates explicit defaults, when the API could not enforce one, so it stays overridable (as agreed in the issue).PolicyService::saveSystem():nullnow means "no explicit system rule" and clears it (clearSystem()), instead of being turned into the default and saved.nulland an explicit default differ there too. The composite validation already treatednullas the default, which is what the effective value becomes.value: null, so the frontend needs no change. Saving the default with "do not allow overrides" now just works.nullas removing the explicit rule. Before, they said it reset the value to its default.Direct reads and bulk loading share
loadSystemPolicy()(resolveMany()goes throughresolveCore()), so they stay consistent. Rule counts already come from the stored key, so an enforced default is counted (existingtestLoadAllRuleCountsCountsExplicitSystemRuleEvenWhenValueMatchesDefault).I also checked the internal callers of
saveSystem():AEngineHandler(enginenone) saves a value different from the default, which was already an enforced rule.SignatureTextService::save()has no production caller.Composition (2 commits): the fix is 70 lines of production code and 209 lines of tests. The regenerated OpenAPI/TypeScript commit is 16 lines, descriptions only.
🧪 How to test
POST /ocs/v2.php/apps/libresign/api/v1/policies/system/show_confetti_after_signingwithvalue = true,allowChildOverride = false(the default value, enforced).PUT …/policies/user/signer1/show_confetti_after_signingwithvalue = false.signer1,GET …/policies/effective:show_confetti_after_signingistruefromglobal. Onmainit isfalsefromuser_policy.POST …/policies/system/show_confetti_after_signingwithvalue = null, then the sameGETassigner1: nowfalsefromuser_policy.This is the new scenario in
features/policies/boolean_policy_layers.feature. It fails onmainat step 3.Validation evidence
PolicySourceTestresolves the issue's matrix throughDefaultPolicyResolver, against a conflicting group layer and a conflicting user layer, checking the effective value. The matrix covers no rule, an explicit default enforced or overridable, an explicit value enforced or overridable, and a legacy default without flag.PolicyServiceTest:nullclears and an explicit default saves, also in a compound write that mixes both.composer cs:checkis clean, and psalm adds no issue.features/policies/boolean_policy_layers.feature(4 scenarios) pass.⚙️ API / Back-end changes
nullremoves the explicit system rule (single and compound writes).composer openapiwhen necessary✅ Checklist
🤖 AI (if applicable)